﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Affinity.Core.Validation;

namespace Affinity.Flux
{
    /// <summary>
    /// Represents a parent component exposing one or more child component references via a particular property.
    /// Monitors for property and collection change events.
    /// </summary>
    internal class ChildReference
    {
        /// <summary>
        /// Factory method, creates a ChildReference for the specified definition and component.
        /// </summary>
        /// <param name="childReferenceDefinition"></param>
        /// <param name="component"></param>
        /// <returns></returns>
        public static ChildReference Create(ChildReferenceDefinition childReferenceDefinition, object component)
        {
            Assert.ArgumentNotNull(childReferenceDefinition, "childReferenceDefinition");
            return new ChildReference(component, childReferenceDefinition.Member);
        }

        /// <summary>
        /// Factory method for creating a ChildReference for the specified parent component exposing one or more child component references via the specified property,
        /// and whether to automatically register child component(s).
        /// </summary>
        /// <param name="component"></param>
        /// <param name="property"></param>
        /// <param name="autoRegister"></param>
        /// <returns></returns>
        public static ChildReference Create(object component, PropertyInfo property, bool autoRegister = true)
        {
            return new ChildReference(component, property);
        }

        /// <summary>
        /// Constructor accepting a reference to the parent component, the property exposing one or more child component references, and whether to automatically
        /// register the child component(s).
        /// </summary>
        /// <param name="component"></param>
        /// <param name="property"></param>
        /// <param name="autoRegister"></param>
        protected ChildReference(object component, PropertyInfo property, bool autoRegister = true)
        {
            _Component = component;
            _Property = property;
            _PropertyIsEnumeration = typeof(IEnumerable).IsAssignableFrom(property.PropertyType);
            _AutoRegister = autoRegister;

            if (component is INotifyPropertyChanged) ((INotifyPropertyChanged)component).PropertyChanged += HandlePropertyChanged;
            HandlePropertyChanged();
        }

        /// <summary>
        /// Raised when a child component reference is added.
        /// </summary>
        public event Action<object> ChildComponentAdded;

        /// <summary>
        /// Raised when a child component reference is removed.
        /// </summary>
        public event Action<object> ChildComponentRemoved;

        /// <summary>
        /// Gets whether to automatically register new child components.
        /// </summary>
        public bool AutoRegister
        {
            get { return _AutoRegister; }
        }

        /// <summary>
        /// Gets an array of current child component references.
        /// </summary>
        public object[] ChildComponents
        {
            get { return _ChildComponents.ToArray(); }
        }

        /// <summary>
        /// Gets the parent component.
        /// </summary>
        public object Component
        {
            get { return _Component; }
        }

        /// <summary>
        /// Returns a description of this object.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0}.{1} (Hash: {2})", _Component.GetType().Name, _Property.Name, GetHashCode());
        }
        
        /// <summary>
        /// Adds a child component reference.
        /// </summary>
        /// <param name="childComponent"></param>
        private void AddChildComponent(object childComponent)
        {
            if (childComponent != null && !_ChildComponents.Contains(childComponent))
            {
                _ChildComponents.Add(childComponent);
                RaiseChildComponentAdded(childComponent);
            }
        }

        /// <summary>
        /// Handles PropertyChanged events on the parent component for the appropriate property.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == _Property.Name) HandlePropertyChanged();
        }

        /// <summary>
        /// Handles appropriate PropertyChanged events on the parent component.
        /// </summary>
        private void HandlePropertyChanged()
        {
            object newPropertyValue = _Property.GetValue(_Component, null);
            
            if (!_PropertyIsEnumeration)
            {
                if (!object.Equals(_PropertyValue, newPropertyValue))
                {
                    RemoveChildComponent(_PropertyValue);
                    _PropertyValue = newPropertyValue;
                    AddChildComponent(_PropertyValue);
                }
            }
            else
            {
                if (_PropertyValue is INotifyCollectionChanged) ((INotifyCollectionChanged)_PropertyValue).CollectionChanged -= HandlePropertyCollectionChanged;
                _PropertyValue = newPropertyValue;
                if (_PropertyValue is INotifyCollectionChanged) ((INotifyCollectionChanged)_PropertyValue).CollectionChanged += HandlePropertyCollectionChanged;

                object[] currentChildComponents = (newPropertyValue as IEnumerable ?? new object[0]).Cast<object>().ToArray();
                object[] removedChildComponents = _ChildComponents.Except(currentChildComponents).ToArray();
                object[] addedChildComponents = currentChildComponents.Except(_ChildComponents).ToArray();

                foreach (var removedChildComponent in removedChildComponents) RemoveChildComponent(removedChildComponent);
                foreach (var addedChildComponent in addedChildComponents) AddChildComponent(addedChildComponent);
            }
        }

        /// <summary>
        /// Handles the CollectionChanged event on the collection exposing child component references, when the appropriate property
        /// exposes child components via a collection implementing INotifyCollectionChanged.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandlePropertyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.OldItems != null) foreach (object oldItem in e.OldItems) RemoveChildComponent(oldItem);
            if (e.NewItems != null) foreach (object newItem in e.NewItems) AddChildComponent(newItem);
        }

        /// <summary>
        /// Raises the ChildComponentAdded event.
        /// </summary>
        /// <param name="childComponent"></param>
        private void RaiseChildComponentAdded(object childComponent)
        {
            if (ChildComponentAdded != null) ChildComponentAdded(childComponent);
        }

        /// <summary>
        /// Raises the ChildComponentRemoved event.
        /// </summary>
        /// <param name="childComponent"></param>
        private void RaiseChildComponentRemoved(object childComponent)
        {
            if (ChildComponentRemoved != null) ChildComponentRemoved(childComponent);
        }

        /// <summary>
        /// Removes a child component reference.
        /// </summary>
        /// <param name="childComponent"></param>
        private void RemoveChildComponent(object childComponent)
        {
            if (childComponent != null && _ChildComponents.Contains(childComponent))
            {
                _ChildComponents.Remove(childComponent);
                RaiseChildComponentRemoved(childComponent);
            }
        }

        private readonly bool _AutoRegister;
        private readonly HashSet<object> _ChildComponents = new HashSet<object>();
        private readonly object _Component;
        private readonly bool _PropertyIsEnumeration;
        private readonly PropertyInfo _Property;
        private object _PropertyValue;
    }
}
